﻿/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using AAEngine.Engine.Debug;
using AAEngine.Engine.Entities;
using AAEngine.Engine.Resources;
using AAEngine.Engine.Serialization;

namespace AAEngine.Engine.Core
{
    /// <summary>
    /// The template manager loads and unloads level files and stores information
	/// about their contents. The Serializer is used to deserialize object
	/// descriptions.
    /// </summary>
    /// <remarks>
    /// A level file can contain templates, entities, and groups. A template
	/// describes an entity that will be instantiated several times, like a
	/// bullet. Templates are left unnamed when they are instantiated.
    /// 
    /// An entity describes a complete entity that is only instantiated once, like
	/// a background tilemap. Entities are named based on the name of the xml data
	/// that describes it.
    /// 
    /// A group contains references to templates, entities, and other groups that
	/// should be instantiated when the group is instantiated.
    /// </remarks>
    public class TemplateManager : EventDispatcher
    {
        public static readonly string FILE_LOADED_EVENT = "AAEngine.Engine.Core.TemplateManager.FILES_LOADED_EVENT";

        /// <summary>
        /// Report every time we create an entity.
        /// </summary>
        public static bool VerboseLogging { get; set; }

        /// <summary>
        /// Loads a level file and adds its contents to the template manager. This
		/// does not instantiate any of the objects in the file, it merely loads
		/// them for future instantiation.
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="forceLoad"></param>
#if WINDOWS_PHONE || XBOX
        public void LoadFile(string filename)
        {
            LoadFile(filename, false);
        }

        public void LoadFile(string filename, bool forceLoad )
#else
        public void LoadFile(string filename, bool forceLoad = false)
#endif
        {
            XmlResource xml = (XmlResource)AAGame.ResourceManager.Load(filename, typeof(XmlResource), forceLoad);

            int version = Int32.Parse(xml.Data.GetAttribute("version"));
            int thingCount = 0;
            foreach (XElement node in xml.Data.Elements())
            {
                thingCount++;
                AddXml(node, filename, version);
            }

            Logger.Print(this, "Loaded '" + thingCount + "' from '" + filename + "'");

            DispatchEvent(new Event(FILE_LOADED_EVENT));
        }

        /// <summary>
        /// Unloads a level file and removes its contents from the template manager.
		/// This does not destroy any entities that have been instantiated.
        /// </summary>
        /// <param name="filename"></param>
        public void UnloadFile(string filename)
        {
            RemoveXml(filename);
            AAGame.ResourceManager.Unload(filename);
        }

        /// <summary>
        /// Creates an instance of an object with the specified name. The name must
		/// refer to a template or entity. To instantiate groups, use instantiateGroup
		/// instead.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="entityName"></param>
        /// <returns></returns>
#if WINDOWS_PHONE || XBOX
        public IEntity InstantiateEntity(string name)
        {
            return InstantiateEntity(name, null);
        }

        public IEntity InstantiateEntity(string name, string entityName)
#else
        public IEntity InstantiateEntity(string name, string entityName = null)
#endif
        {
            Profiler.Enter("InstantiateEntity");

            //try
            //{
                IEntity entity;
                if (_things.ContainsKey(name))
                {
                    if (_things[name].GroupCallback != null)
                    {
                        throw new AAException("Thing '" + name + "' is a group callback!");
                    }

                    if (_things[name].EntityCallback != null)
                    {
                        entity = _things[name].EntityCallback();
                        Profiler.Exit("InstantiateEntity");
                        return entity;
                    }
                }

                XElement xml = GetXml(name, "template", "entity");
                if (xml == null)
                {
                    Logger.Error(this, "InstantiateEntity", "Unable to find a template or entity with the name " + name + ".");
                    Profiler.Exit("InstantiateEntity");
                    return null;
                }

                entity = InstantiateEntityFromXml(xml, entityName);
                Profiler.Exit("InstantiateEntity");
                return entity;
            //}
            //catch (Exception ex)
            //{
            //    Logger.Error(this, "InstantiateEntity", "Failed instantiating '" + name + "' due to: " + ex.Message + "\n" + ex.StackTrace);
            //    Profiler.Exit("InstantiateEntity");
            //    return null;
            //}
        }

#if WINDOWS_PHONE || XBOX
        public IEntity InstantiateEntityFromXml(XElement xml)
        {
            return InstantiateEntityFromXml(xml, null);
        }

        public IEntity InstantiateEntityFromXml(XElement xml, string entityName)
#else
        public IEntity InstantiateEntityFromXml(XElement xml, string entityName = null)
#endif
        {
            Profiler.Enter("InstantiateEntityFromXml");
            string name = "";
            //try
            //{
                name = xml.GetAttribute("name");
                if (xml.Name == "template")
                {
                    name = (entityName == null) ? "" : entityName;
                }

                string alias = xml.GetAttribute("alias");
                if (alias == "")
                {
                    alias = null;
                }

                IEntity entity = AAGame.NewEntity();

                entity.Initialize(name, alias);
                entity.Deferring = true;

                if (!DoInstantiateTemplate(entity, xml.GetAttribute("template"), new Dictionary<string, bool>()))
                {
                    entity.Destroy();
                    Profiler.Exit("InstantiateEntityFromXml");
                    return null;
                }

                Serializer.Deserialize(entity, xml);
                Serializer.ClearCurrentEntity();

                entity.Deferring = false;

                if (!_inGroup)
                {
                    Serializer.ReportMissingReferences();
                }

                Profiler.Exit("InstantiateEntityFromXml");
                return entity;
            //}
            //catch (Exception ex)
            //{
            //    Logger.Error(this, "InstantiateEntityFromXml", "Failed instantiating '" + name + "' due to: " + ex.Message + "\n" + ex.StackTrace);
            //    Profiler.Exit("InstantiateEntityFromXml");
            //    return null;
            //}
        }

        /// <summary>
        /// instantiates all templates or entities referenced by the specified group.
        /// </summary>
        /// <param name="name">The name of the group to instantiate. This correspands to the
		/// name attribute on the group tag in the XML.</param>
        /// <returns>An array containing all the instantiated objects. If the group
		/// wasn't found, the array will be empty.</returns>
        public IEnumerable<IEntity> InstantiateGroup(string name)
        {
            if (_things.ContainsKey(name))
            {
                if (_things[name].EntityCallback != null)
                {
                    throw new AAException("Thing '" + name + "' is an entity callback!");
                }

                // We won't dispatch the GROUP_LOADED event here as it's the callback
                // author's responsibility.
                if (_things[name].GroupCallback != null)
                {
                    return _things[name].GroupCallback();
                }
            }
            //try
            //{
                List<IEntity> group = new List<IEntity>();
                if (!DoInstantiateGroup(name, group, new Dictionary<string, bool>()))
                {
                    foreach (IEntity entity in group)
                    {
                        entity.Destroy();
                    }
                    return null;
                }

                if (HasEventListeners(TemplateEvent.GROUP_LOADED_EVENT))
                {
                    DispatchEvent(new TemplateEvent(TemplateEvent.GROUP_LOADED_EVENT, name));
                }

                return group;
            //}
            //catch (Exception ex)
            //{
            //    Logger.Error(this, "InstantiateGroup", "Failed to instantiate group '" + name + "' due to: " + ex.Message + "\n" + ex.StackTrace);
            //    return null;
            //}
        }

        /// <summary>
        /// Adds an XML description of a template, entity, or group to the template manager so
		/// it can be instantiated in the future.
        /// </summary>
        /// <param name="xml">The xml to add.</param>
        /// <param name="identifier">A string by which this xml can be referenced. This is NOT the
		/// name of the object. It is used so the xml can be removed by a call to RemoveXML.</param>
        /// <param name="version">The version of the format of the added xml.</param>
        public void AddXml(XElement xml, string identifier, int version)
        {
            string name = xml.GetAttribute("name");

            if (name == "")
            {
                Logger.Warn(this, "AddXml", "XML object description added with a 'name' attribute.");
                return;
            }
            if (_things.ContainsKey(name))
            {
                Logger.Warn(this, "AddXml", "An XML object description with name '" + name + "' has already been added.");
                return;
            }

            ThingReference thing = new ThingReference();
            thing.XmlData = xml;
            thing.Identitifer = identifier;
            thing.Version = version;
            _things[name] = thing;
        }

        /// <summary>
        /// Removes the specified object from the template manager.
        /// </summary>
        /// <param name="identifier">This is NOT the name of the xml object. It is the value
		/// passed as the identifier in AddXML.</param>
        public void RemoveXml(string identifier)
        {
            List<string> thingsToDelete = new List<string>();

            foreach (string key in _things.Keys)
            {
                if (_things[key].Identitifer == identifier)
                {
                    thingsToDelete.Add(key);
                }
            }

            foreach (string key in thingsToDelete)
            {
                _things.Remove(key);
            }
        }

        /// <summary>
        /// Gets a previously added xml description that has the specified name.
        /// </summary>
        /// <param name="name">The name of the xml to retrieve.</param>
        /// <param name="xmlType1">The type (template, entity, or group) the xml must be.
		/// If this is null, it can be anything.</param>
        /// <param name="xmlType2">Another type (template, entity, or group) the xml can
		/// be.</param>
        /// <returns>The xml description with the specified name, or null if it wasn't
		/// found.</returns>
#if WINDOWS_PHONE || XBOX
        public XElement GetXml(string name)
        {
            return GetXml(name, null, null);
        }

        public XElement GetXml(string name, string xmlType1)
        {
            return GetXml(name, xmlType1, null);
        }

        public XElement GetXml(string name, string xmlType1, string xmlType2)
#else
        public XElement GetXml(string name, string xmlType1 = null, string xmlType2 = null)
#endif
        {
            ThingReference thing = DoGetXml(name, xmlType1, xmlType2);
            return thing != null ? thing.XmlData : null;
        }

        /// <summary>
        /// Check if a template method by the provided name has been registered.
        /// </summary>
        /// <param name="name">Name of the template registered with the TemplateManager</param>
        /// <returns>true if the template exists, false if it does not.</returns>
        public bool HasEntityCallback(string name)
        {
            return _things.ContainsKey(name);
        }

        /// <summary>
        /// Register a callback-powered entity with the TemplateManager. Instead of
		/// parsing and returning an entity based on XML, this lets you directly
		/// create the entity from a function you specify.
        /// </summary>
        /// <remarks>Generally, we recommend using XML for entity definitions, but this can
		/// be useful for reducing external dependencies, or providing special
		/// functionality (for instance, a single name that returns several
		/// possible entities based on chance).</remarks>
        /// <param name="name">Name of the entity.</param>
        /// <param name="callback">A function which takes no arguments and returns an IEntity.</param>
        public void RegisterEntityCallback(string name, Func<IEntity> callback)
        {
            if (callback == null)
            {
                throw new AAException("Must pass a callback function!");
            }

            if (_things.ContainsKey(name))
            {
                throw new AAException("Already have a thing registered under '" + name + "'!");
            }

            ThingReference thing = new ThingReference();
            thing.EntityCallback = callback;
            _things[name] = thing;
        }

        /// <summary>
        /// Unregister a callback-powered entity registered with RegisterEntityCallback.
        /// </summary>
        /// <param name="name">Name passed to RegisterEntityCallback.</param>
        public void UnregisterEntityCallback(string name)
        {
            if (!_things.ContainsKey(name))
            {
                Logger.Warn(this, "UnregisterEntityCallback", "No such template '" + name + "'!");
                return;
            }

            if (_things[name].EntityCallback == null)
            {
                throw new AAException("Thing '" + name + "' is not an entity callback!");
            }

            _things.Remove(name);
        }

        /// <summary>
        /// Register a function as a group. When the group is requested via instantiateGroup,
		/// the function is called, and the Array it returns is given to the user.
        /// </summary>
        /// <param name="name">Name of the group.</param>
        /// <param name="callback">A function which takes no arguments and returns an array of IEntity instances.</param>
        public void RegisterGroupCallback(string name, Func<IEnumerable<IEntity>> callback)
        {
            if (callback == null)
            {
                throw new AAException("Must pass a callback function!");
            }

            if (_things.ContainsKey(name))
            {
                throw new AAException("Already have a thing registered under '" + name + "'!");
            }

            ThingReference thing = new ThingReference();
            thing.GroupCallback = callback;
            _things[name] = thing;
        }

        /// <summary>
        /// Unregister a function-based group registered with RegisterGroupCallback.
        /// </summary>
        /// <param name="name">Name passed to RegisterGroupCallback.</param>
        public void UnregisterGroupCallback(string name)
        {
            if (!_things.ContainsKey(name))
            {
                Logger.Warn(this, "UnregisterGroupCallback", "No such template '" + name + "'!");
                return;
            }

            if (_things[name].GroupCallback == null)
            {
                throw new AAException("Thing '" + name + "' is not a groiup callback!");
            }

            _things.Remove(name);
        }

        private ThingReference DoGetXml(string name, string xmlType1, string xmlType2)
        {
            if (!_things.ContainsKey(name))
            {
                return null;
            }

            ThingReference thing = _things[name];
            if (thing.EntityCallback != null || thing.GroupCallback != null)
            {
                return null;
            }

            if (xmlType1 != null)
            {
                string type = thing.XmlData.Name.LocalName;
                if (type != xmlType1 && type != xmlType2)
                {
                    return null;
                }
            }

            return thing;
        }

        private bool DoInstantiateTemplate(IEntity entity, string templateName, Dictionary<string, bool> tree)
        {
            if (String.IsNullOrEmpty(templateName))
            {
                return true;
            }

            if (tree.ContainsKey(templateName))
            {
                Logger.Warn(this, "DoInstantiateTemplate", "Cyclical template detected. " + templateName + " has already been instantiated.");
                return false;
            }

            XElement templateXml = GetXml(templateName, "template");
            if (templateXml == null)
            {
                Logger.Warn(this, "DoInstantiateTemplate", "Unable to find the template '" + templateName + "'.");
                return false;
            }

            tree[templateName] = true;

            if (!DoInstantiateTemplate(entity, templateXml.GetAttribute("template"), tree))
            {
                return false;
            }

            entity.Deserialize(templateXml, false);

            return true;
        }

        private bool DoInstantiateGroup(string name, IList<IEntity> group, Dictionary<string, bool> tree)
        {
            XElement xml = GetXml(name, "group");

            if (xml == null)
            {
                throw new AAException("Coult not find group '" + name + "'.");
            }

            AAGroup actualGroup = new AAGroup();
            if (name != AAGame.RootGroup.Name)
            {
                actualGroup.Initialize(name);
                actualGroup.OwningGroup = AAGame.CurrentGroup;
            }
            else
            {
                actualGroup = AAGame.RootGroup;
            }

            AAGroup oldGroup = AAGame.CurrentGroup;
            AAGame.CurrentGroup = actualGroup;

            foreach(XElement node in xml.Elements())
            {
                string childName = node.GetAttribute("name");
                if (node.Name == "groupReference")
                {
                    if (tree[childName])
                    {
                        throw new AAException("Cyclical group detected. '" + childName + "' has already been instantiated.");
                    }
                    tree[childName] = true;

                    try
                    {
                        if (!DoInstantiateGroup(childName, group, tree))
                        {
                            return false;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Warn(this, "DoInstantiateGroup", "Failed to instantiate group  '" + childName + "' from groupReference in '" + name + "' due to: " + ex.Message);
                        return false;
                    }
                }
                else if (node.Name == "objectReference")
                {
                    _inGroup = true;
                    group.Add(InstantiateEntity(childName));
                    _inGroup = false;
                }
                else
                {
                    Logger.Warn(this, "DoInstantiateGroup", "Encountered unknown tag '" + node.Name + "' in group.");
                }
            }

            AAGame.CurrentGroup = oldGroup;

            Serializer.ReportMissingReferences();

            return true;
        }

        private bool _inGroup = false;
        private Dictionary<string, ThingReference> _things = new Dictionary<string, ThingReference>();
    }

    class ThingReference
    {
        public int Version { get; set; }
        public XElement XmlData { get; set; }
        public Func<IEntity> EntityCallback { get; set; }
        public Func<IEnumerable<IEntity>> GroupCallback { get; set; }
        public string Identitifer { get; set; }
    }
}
